package org.atlantis.grosbeak.lang;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.atlantis.grosbeak.lang.codec.Charsets;
import org.atlantis.grosbeak.lang.filefilter.EveryFileFilter;

/**
 * 对文件进行操作的帮助类。
 * 
 * @author <a href="mailto:seenoevil.cn@gmail.com">SeeNoEvil</a>
 * 
 */
public abstract class Files {

	/**
	 * 1KB中所包含的字节数。
	 */
	public static final long ONE_KB = 1024;

	/**
	 * 1MB中所包含的字节数
	 */
	public static final long ONE_MB = ONE_KB * ONE_KB;

	/**
	 * 1GB中所包含的字节数。
	 */
	public static final long ONE_GB = ONE_MB * ONE_MB;

	/**
	 * 拓展名分隔符。
	 */
	public static final char EXTENSION_SEPARATOR = '.';

	/**
	 * 当前系统分隔符。
	 */
	public static final char SYSTEM_SEPARATOR = File.separatorChar;

	// Windows系统的系统分隔符。
	private static final char WINDOWS_SEPARATOR = '\\';

	// Unix、Linux系统的系统分隔符。
	private static final char UNIX_SEPARATOR = '/';

	/**
	 * 在ClassPath下查找一个文件。
	 * 
	 * @param path
	 *            文件相对于clazz类的路径。
	 * @param clazz
	 *            参考类，在查找时会使用到它的ClassLoader。
	 * @param enc
	 *            文件路径的编码。
	 * @return 文件对象。
	 */
	public static File find(String path, Class<?> clazz, String enc) {
		if (null == path)
			return null;
		path = Charsets.decode(path);
		File file = new File(path);
		if (!file.exists()) {
			file = null;
			URL url = null;
			if (null != clazz) {
				url = clazz.getResource(path);
				if (null == url)
					url = clazz.getClassLoader().getResource(path);
			}
			if (null == url)
				url = ClassLoader.getSystemResource(path);
			if (null != url) {
				file = new File(Charsets.decode(url.getPath(), enc));
			}
		}
		return file;
	}

	/**
	 * 在ClassPath下查找一个文件。
	 * 
	 * @param path
	 *            文件相对于此类（Files.class）的路径。
	 * @param enc
	 *            文件路径的编码。
	 * @return 文件对象。
	 */
	public static File find(String path, String enc) {
		return find(path, Files.class, enc);
	}

	/**
	 * 在ClassPath下查找一个文件（采用本地编码）。
	 * 
	 * @param path
	 *            文件相对于此类（Files.class）的路径。
	 * @return 文件对象。
	 */
	public static File find(String path) {
		return find(path, Files.class, Charsets.DEFAULT_CHARSET);
	}

	/**
	 * 通过正则表达式从zip压缩包中查找文件。
	 * 
	 * @param zip
	 *            ZipFile对象。
	 * @param regex
	 *            查找条件的正则表达式。
	 * @return 查找到的结果数组。
	 */
	public static ZipEntry[] findInZip(ZipFile zip, String regex) {
		List<ZipEntry> entries = new LinkedList<ZipEntry>();
		Enumeration<? extends ZipEntry> enums = zip.entries();
		while (enums.hasMoreElements()) {
			ZipEntry entry = enums.nextElement();
			if (null == regex || entry.getName().matches(regex))
				entries.add(entry);
		}
		return entries.toArray(new ZipEntry[entries.size()]);
	}

	/**
	 * 把文件的大小（以字节计算），转换成一个可读性更高的数据。如：
	 * 
	 * <pre>
	 * displaySize(1024L);返回的就是1KB。
	 * </pre>
	 * 
	 * 
	 * @param size
	 *            文件的大小。
	 * @return
	 */
	public static String displaySize(long size) {
		String displaySize = "";
		if (size / ONE_GB > 0) {
			displaySize = String.valueOf(size / ONE_GB) + "GB";
		} else if (size / ONE_MB > 0) {
			displaySize = String.valueOf(size / ONE_MB) + "MB";
		} else if (size / ONE_KB > 0) {
			displaySize = String.valueOf(size / ONE_KB) + "KB";
		} else {
			displaySize = String.valueOf(size) + "B";
		}
		return displaySize;
	}

	/**
	 * 从一个目录（dir）中找到所有符合（filter）的文件，并装入到files集合中。
	 * 
	 * @param files
	 *            存放所有目标文件的集合。
	 * @param dir
	 *            需要查找的目录。
	 * @param filter
	 *            文件过滤器。
	 */
	private static void innerListFiles(Collection<File> files, File dir,
			FileFilter filter) {
		if (filter == null) {
			filter = new EveryFileFilter();
		}
		File[] founds = dir.listFiles(filter);
		if (founds != null) {
			for (File found : founds) {
				if (found.isDirectory()) {
					innerListFiles(files, found, filter);
				} else {
					files.add(found);
				}
			}
		}
	}

	/**
	 * 在指定目录下（dir）查找所有符合条件（filter）的文件，然后装入到一个集合中返回。
	 * 
	 * @param dir
	 *            需要查找的目录。
	 * @param filter
	 *            文件过滤器。如果为‘null’则表示所有文件都符合。
	 * @return 包含所有目标文件的集合。
	 * 
	 * @throws FileIOException
	 *             当参数dir不是一个目录时抛出。
	 */
	public static Collection<File> listFiles(File dir, FileFilter filter)
			throws FileIOException {
		if (!dir.isDirectory())
			throw Exceptions.makeThrow(FileIOException.class,
					"The parameter 'dir' is not a directory.");
		Collection<File> files = new LinkedList<File>();
		innerListFiles(files, dir, filter);
		return files;
	}

	/**
	 * 查找最后一个系统分隔符的位置。
	 * 
	 * @param filename
	 *            需要查找最后一个系统分隔符的文件名。如果filename为null，则返回-1。
	 * @return 最后一个系统分隔符的位置。
	 */
	public static int indexOfLastSeparator(String filename) {
		if (filename == null)
			return -1;
		int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
		int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
		return Math.max(lastUnixPos, lastWindowsPos);
	}

	/**
	 * 查找文件拓展名分隔符的位置。文件拓展名分隔符一般是“.”。
	 * 
	 * @param filename
	 *            需要查找拓展名分隔符位置的文件名。
	 * @return 文件拓展名分隔符的位置。
	 */
	public static int indexOfExtension(String filename) {
		if (filename == null)
			return -1;
		int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
		int lastSeparator = indexOfLastSeparator(filename);
		return (lastSeparator > extensionPos ? -1 : extensionPos);
	}

	/**
	 * 从文件路径得到文件名。如：
	 * 
	 * <pre>
	 * software/java/readme.txt --> readme.txt
	 * readme.txt               --> readme.txt
	 * software/java/readme     --> readme
	 * software/java/           --> ""
	 * </pre>
	 * 
	 * @param filename
	 * @return
	 */
	public static String getName(String filename) {
		if (filename == null)
			return null;
		int index = indexOfLastSeparator(filename);
		return filename.substring(index + 1);
	}

	/**
	 * 获得基本文件名。如：
	 * 
	 * <pre>
	 * software/java/readme.txt --> readme
	 * readme.txt               --> readme
	 * software/java/readme     --> readme
	 * software/java/lib/       --> ""
	 * </pre>
	 * 
	 * @param filename
	 * @return
	 */
	public static String getBaseName(String filename) {
		return removeExtension(getName(filename));
	}

	/**
	 * 去除文件拓展名。如：
	 * 
	 * <pre>
	 * readme.txt                 --> readme
	 * photos\schools\friends.png --> photos\schools\friends
	 * software\java\lib          --> software\java\lib
	 * </pre>
	 * 
	 * @param filename
	 *            需要去除文件拓展名的文件名。
	 * @return 去除拓展名后的文件名。
	 */
	public static String removeExtension(String filename) {
		if (filename == null)
			return null;
		int index = indexOfExtension(filename);
		if (index == -1)
			return filename;
		else
			return filename.substring(0, index);
	}

	/**
	 * 获得文件拓展名。如：
	 * 
	 * <pre>
	 * readme.txt                --> "txt"
	 * photos/school/friends.png --> "png"
	 * software/A.java/lib       --> ""
	 * software/java/lib         --> ""
	 * </pre>
	 * 
	 * @param filename
	 * @return
	 */
	public static String getExtension(String filename) {
		if (filename == null)
			return null;
		int index = indexOfExtension(filename);
		if (index == -1)
			return "";
		else
			return filename.substring(index + 1);
	}

	/**
	 * 得到文件的根目录（在Windows下根目录是盘，如C盘、D盘等；在Unix下是文件夹）。如：
	 * 
	 * @param filename
	 *            需要得到根目录的文件名。
	 * @return 根目录。
	 */
	public static String getPrefix(String filename) {
		if (filename == null)
			return null;
		int length = getPrefixLength(filename);
		if (length < 0)
			return null;
		if (length > filename.length())
			return filename + UNIX_SEPARATOR;
		return filename.substring(0, length);
	}

	/**
	 * 得到文件的目录（包括最后一个分隔符）。
	 * 
	 * @param filename
	 *            需要得到文件目录的文件名。
	 * @return 文件目录。
	 */
	public static String getFullPath(String filename) {
		return doGetFullPath(filename, true);
	}

	/**
	 * 得到文件的目录（不包括最后一个分隔符）。
	 * 
	 * @param filename
	 *            需要得到的文件目录的文件名。
	 * @return 文件目录。
	 */
	public static String getFullPathWithoutEndSeparator(String filename) {
		return doGetFullPath(filename, false);
	}

	// 得到文件的路径。
	private static String doGetFullPath(String filename,
			boolean includeSeparator) {
		if (filename == null)
			return null;
		int prefix = getPrefixLength(filename);
		if (prefix < 0)
			return null;
		if (prefix > filename.length())
			if (includeSeparator)
				return getPrefix(filename);
			else
				return filename;
		int index = indexOfLastSeparator(filename);
		if (index < 0)
			return filename.substring(0, prefix);
		int end = index + (includeSeparator ? 1 : 0);
		return filename.substring(0, end);
	}

	// 得到前缀长度。
	private static int getPrefixLength(String filename) {
		if (filename == null) {
			return -1;
		}
		int len = filename.length();
		if (len == 0) {
			return 0;
		}
		char ch0 = filename.charAt(0);
		if (ch0 == ':') {
			return -1;
		}
		if (len == 1) {
			if (ch0 == '~') {
				return 2; // return a length greater than the input
			}
			return (isSeparator(ch0) ? 1 : 0);
		} else {
			if (ch0 == '~') {
				int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
				int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
				if (posUnix == -1 && posWin == -1) {
					return len + 1; // return a length greater than the input
				}
				posUnix = (posUnix == -1 ? posWin : posUnix);
				posWin = (posWin == -1 ? posUnix : posWin);
				return Math.min(posUnix, posWin) + 1;
			}
			char ch1 = filename.charAt(1);
			if (ch1 == ':') {
				ch0 = Character.toUpperCase(ch0);
				if (ch0 >= 'A' && ch0 <= 'Z') {
					if (len == 2 || isSeparator(filename.charAt(2)) == false) {
						return 2;
					}
					return 3;
				}
				return -1;

			} else if (isSeparator(ch0) && isSeparator(ch1)) {
				int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
				int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
				if ((posUnix == -1 && posWin == -1) || posUnix == 2
						|| posWin == 2) {
					return -1;
				}
				posUnix = (posUnix == -1 ? posWin : posUnix);
				posWin = (posWin == -1 ? posUnix : posWin);
				return Math.min(posUnix, posWin) + 1;
			} else {
				return (isSeparator(ch0) ? 1 : 0);
			}
		}
	}

	// 判断一个字符是否是系统分隔符。
	private static boolean isSeparator(char ch) {
		return (ch == UNIX_SEPARATOR) || (ch == WINDOWS_SEPARATOR);
	}

	/**
	 * 判断一个文件是否是指定类型。
	 * 
	 * @param filename
	 *            文件名。
	 * @param extension
	 *            指定拓展名。
	 * @return 判断这个文件是否是指定类型。
	 */
	public static boolean isExtension(String filename, String extension) {
		if (filename == null)
			return false;
		if (extension == null || extension.length() == 0)
			return (indexOfExtension(filename) == -1);
		String fileExt = getExtension(filename);
		return Strings.endsWithIgnoreCase(fileExt, extension);
	}

	/**
	 * 判断一个文件是否是制定类型。
	 * 
	 * @param filename
	 *            文件名。
	 * @param extensions
	 *            指定拓展名数组。
	 * @return 判断这个文件是否是指定类型。
	 */
	public static boolean isExtension(String filename, String[] extensions) {
		if (filename == null)
			return false;
		if (extensions == null || extensions.length == 0)
			return (indexOfExtension(filename) == -1);
		String fileExt = getExtension(filename);
		for (String extension : extensions) {
			if (Strings.equalsIgnoreCase(fileExt, extension))
				return true;
		}
		return false;
	}

	/**
	 * 删除一个文件。如果这个文件是目录，那么就把这个目录和这个目录下所有的文件都删除。
	 * 
	 * @param file
	 *            要删除的文件。
	 * @throws FileIOException
	 *             当不能删除指定文件时抛出。
	 */
	public static void delete(File file) throws FileIOException {
		if (file.isDirectory()) {
			deleteDirectory(file);
		} else {
			boolean filePresent = file.exists();
			if (!file.delete()) {
				if (!filePresent)
					throw Exceptions.makeThrow(FileIOException.class,
							"File does not exist: [%s].", file);
				throw Exceptions.makeThrow(FileIOException.class,
						"Unable to delete file: [%s].", file);
			}
		}
	}

	/**
	 * 删除一个目录。如果目录下还有其他文件，则一起删除。
	 * 
	 * @param dir
	 *            要删除的目录。
	 * @throws FileIOException
	 *             在删除目录遇到问题时抛出。
	 */
	public static void deleteDirectory(File dir) throws FileIOException {
		if (!dir.exists())
			return;
		cleanDirectory(dir);
		if (!dir.delete())
			throw Exceptions.makeThrow(FileIOException.class,
					"Unable to delete directory '%s'.", dir);
	}

	/**
	 * 清理一个目录。删除该目录下所有的内容。
	 * 
	 * @param dir
	 *            需要清理的目录。
	 * @throws FileIOException
	 *             当清理过程中出现问题时抛出。
	 */
	public static void cleanDirectory(File dir) throws FileIOException {
		if (!dir.exists())
			throw Exceptions.makeThrow(FileIOException.class,
					"'%s' does not exist.", dir);
		if (!dir.isDirectory())
			throw Exceptions.makeThrow(FileIOException.class,
					"'%s' is not a directory.", dir);
		File files[] = dir.listFiles();
		if (files == null)
			throw Exceptions.makeThrow(FileIOException.class,
					"Failed to list contents of '%s'.", dir);

		FileIOException exception = null;
		for (File file : files) {
			try {
				delete(file);
			} catch (FileIOException e) {
				exception = e;
			}
		}

		if (null != exception)
			throw exception;
	}

	/**
	 * 复制文件。把文件从'src'复制到'dest'，如果dest的目录不存在则自动创建，复制后的文件的最后修改时间就和源文件相同。
	 * 
	 * @param src
	 *            文件源。
	 * @param dest
	 *            文件需要复制到的位置。
	 * @throws FileIOException
	 *             当复制文件出错时抛出
	 */
	public static void copyFile(File src, File dest) throws FileIOException {
		copyFile(src, dest, true);
	}

	/**
	 * 复制文件。把文件从'src'复制到'dest'，如果dest的目录不存在则自动创建。
	 * 
	 * @param src
	 *            文件源。
	 * @param dest
	 *            文件需要复制到的位置。
	 * @param preserveFileDate
	 *            是否保留源文件的最后修改时间。
	 * 
	 * @throws FileIOException
	 *             当复制文件出错时抛出
	 */
	public static void copyFile(File src, File dest, boolean preserveFileDate)
			throws FileIOException {
		if (src == null)
			throw Exceptions.makeThrow(FileIOException.class,
					"'src' must not be null.");
		if (dest == null)
			throw Exceptions.makeThrow(FileIOException.class,
					"'dest' must not be null.");
		if (!src.exists())
			throw Exceptions.makeThrow(FileIOException.class,
					"'%s' dose not exist.", src);
		if (src.isDirectory())
			throw Exceptions.makeThrow(FileIOException.class,
					"'%s' exists but is a directory", src);
		if (src.getPath().equals(dest.getPath()))
			throw Exceptions.makeThrow(FileIOException.class,
					"src '%s' and dest '%s' are the same", src, dest);
		if (dest.getParentFile() != null && !dest.getParentFile().exists()) {
			if (!dest.getParentFile().mkdirs()) {
				throw Exceptions.makeThrow(FileIOException.class,
						"dest '%s' directory can not be created.", dest);
			}
		}
		if (dest.exists() && !dest.canWrite())
			throw Exceptions.makeThrow(FileIOException.class,
					"dest '%s' exists but is read-only.", dest);

		try {
			doCopyFile(src, dest, preserveFileDate);
		} catch (IOException e) {
			throw Exceptions.makeThrow(FileIOException.class,
					"Unable to copy file from '%s' to '%s'.", src, dest);
		}
	}

	/**
	 * 移动文件。把文件从'src'移动到'dest'。
	 * 
	 * @param src
	 *            文件源位置。
	 * @param dest
	 *            文件移动到的位置。
	 * @throws FileIOException
	 */
	public static void moveFile(File src, File dest) throws FileIOException {
		boolean rename = src.renameTo(dest);
		if (!rename) {
			copyFile(src, dest);
			if (!src.delete()) {
				delete(dest);
				throw Exceptions
						.makeThrow(
								FileIOException.class,
								"Failed to delete original file '%s' after copy to '%s'.",
								src, dest);
			}
		}
	}

	// 复制文件。
	private static void doCopyFile(File src, File dest, boolean preserveFileDate)
			throws IOException {
		if (dest.exists() && dest.isDirectory())
			throw Exceptions.makeThrow(FileIOException.class,
					"Destination '%s' exists but is a directory.", dest);
		FileInputStream input = new FileInputStream(src);
		try {
			FileOutputStream output = new FileOutputStream(dest);
			try {
				IOs.copy(input, output);
			} finally {
				IOs.closeQuietly(output);
			}
		} finally {
			IOs.closeQuietly(input);
		}
		if (src.length() != dest.length())
			throw Exceptions.makeThrow(IOException.class,
					"Failed to copy full content from '%s' to '%s'", src, dest);
		if (preserveFileDate)
			dest.setLastModified(src.lastModified());
	}

	/**
	 * 把一段字符串按照指定编码写入到一个文件中。
	 * 
	 * @param file
	 * @param data
	 * @param encoding
	 * @throws FileIOException
	 */
	public static void writeStringToFile(File file, String data, String encoding)
			throws FileIOException {
		OutputStream output = null;
		try {
			output = openOutputStream(file);
			IOs.write(data, output, encoding);
		} finally {
			IOs.closeQuietly(output);
		}
	}

	/**
	 * 把一段字符串写入到一个文件中。
	 * 
	 * @param file
	 * @param data
	 * @throws FileIOException
	 */
	public static void writeStringToFile(File file, String data)
			throws FileIOException {
		writeStringToFile(file, data, null);
	}

	/**
	 * 把多行字符串写入到一个文件中。
	 * 
	 * @param file
	 * @param encoding
	 * @param lines
	 * @param lineEnding
	 * @throws FileIOException
	 */
	public static void writeLinesToFile(File file, String encoding,
			Collection<String> lines, String lineEnding) throws FileIOException {
		OutputStream output = null;
		try {
			output = openOutputStream(file);
			IOs.writeLines(lines, lineEnding, output, encoding);
		} finally {
			IOs.closeQuietly(output);
		}
	}

	/**
	 * 把多行字符串写入到一个文件中。
	 * 
	 * @param file
	 * @param encoding
	 * @param lines
	 * @throws FileIOException
	 */
	public static void writeLinesToFile(File file, String encoding,
			Collection<String> lines) throws FileIOException {
		writeLinesToFile(file, encoding, lines, null);
	}

	/**
	 * 把多行字符串写入到一个文件中。
	 * 
	 * @param file
	 * @param lines
	 * @throws FileIOException
	 */
	public static void writeLinesToFile(File file, Collection<String> lines)
			throws FileIOException {
		writeLinesToFile(file, null, lines, null);
	}

	/**
	 * 从一个文件中按照指定编码读取内容，并把内容存放到一个字符串中。
	 * 
	 * @param file
	 * @param encoding
	 * @return
	 * @throws FileIOException
	 */
	public static String readFileToString(File file, String encoding)
			throws FileIOException {
		InputStream input = null;
		try {
			input = openInputStream(file);
			return IOs.toString(input, encoding);
		} finally {
			IOs.closeQuietly(input);
		}
	}

	/**
	 * 从一个文件中读取所有内容，并把内容存放到一个字符串中。
	 * 
	 * @param file
	 * @return
	 * @throws FileIOException
	 */
	public static String readFileToString(File file) throws FileIOException {
		return readFileToString(file, null);
	}

	/**
	 * 把一个文件中的每一行内容按照指定编码读取出来，并把每一个行内容存放到一个List集合中。
	 * 
	 * @param file
	 * @param encoding
	 * @return
	 * @throws FileIOException
	 */
	public static List<String> readFileToLines(File file, String encoding)
			throws FileIOException {
		InputStream input = null;
		try {
			input = openInputStream(file);
			return IOs.readLines(input, encoding);
		} finally {
			IOs.closeQuietly(input);
		}
	}

	/**
	 * 把一个文件中的每一样内容取出来，并把每一行内容存放到一个List集合中。
	 * 
	 * @param file
	 * @return
	 * @throws FileIOException
	 */
	public static List<String> readFileToLines(File file) throws FileIOException {
		return readFileToLines(file, null);
	}

	/**
	 * 打开一个文件的输出流。
	 * 
	 * @param file
	 * @return
	 * @throws FileIOException
	 */
	public static FileOutputStream openOutputStream(File file)
			throws FileIOException {
		if (file.exists()) {
			if (file.isDirectory())
				throw Exceptions.makeThrow(FileIOException.class,
						"File '%s' exists but is a directory.", file);
			if (!file.canWrite())
				throw Exceptions.makeThrow(FileIOException.class,
						"File '%s' exists but can not be written to.", file);
		} else {
			File parent = file.getParentFile();
			if (parent != null && !parent.exists()) {
				if (!parent.mkdirs()) {
					throw Exceptions.makeThrow(FileIOException.class,
							"File '%s' can not be created.", file);
				}
			}
		}
		try {
			return new FileOutputStream(file);
		} catch (FileNotFoundException e) {
			return null;
		}
	}

	/**
	 * 打开一个文件的输入流。
	 * 
	 * @param file
	 * @return
	 * @throws FileIOException
	 */
	public static FileInputStream openInputStream(File file)
			throws FileIOException {
		if (file.exists()) {
			if (file.isDirectory())
				throw Exceptions.makeThrow(FileIOException.class,
						"File '%s' exists but is a directory.", file);
			if (!file.canRead())
				throw Exceptions.makeThrow(FileIOException.class,
						"File '%s' exists but can not be read.", file);
		} else {
			throw Exceptions.makeThrow(FileIOException.class,
					"File '%s' does not exists.", file);
		}
		try {
			return new FileInputStream(file);
		} catch (FileNotFoundException e) {
			return null;
		}
	}

	/**
	 * 创建一个文件。
	 * 
	 * @param file
	 * @throws FileIOException
	 */
	public static void create(File file) throws FileIOException {
		if (!file.exists()) {
			OutputStream out = openOutputStream(file);
			IOs.closeQuietly(out);
		}
		boolean success = file.setLastModified(System.currentTimeMillis());
		if (!success)
			throw Exceptions.makeThrow(FileIOException.class,
					"Unable to set the last modification time for '%s'.", file);
	}
}
